<?php

/**
 * Базовый класс списков.
 *
 */
abstract class Kernel_Action_List extends Kernel_Action_Abstract {

    /**
     * Ассоциативный массив параметров фильтрации записей и форматов их значений.
     *
     * @var array
     */
    public $filter = array();

    /**
     * Ассоциативный массив полей запроса на параметры фильтрации.
     *
     * @var array
     */
    protected $_filter_map = array();

    /**
     * Количество записей, выводимых на странице. Хочешь включить разбиение —
     * укажи, по сколько записей разбивать.
     *
     * @var integer
     */
    protected $_per_page = 0;

    /**
     * Определяет, можно ли выводить пустой список, или в противном случае
     * нужно генерировать ошибку 404.
     *
     * @var boolean
     */
    protected $_allow_empty = true;

    /**
     * Реализация обработки пользовательского запроса.
     *
     */
    protected function _process() {

        // определяем номер страницы, которую нужно вывести
        $page = $this->_getPage();

        // если указана заведомо неверная страница
        if ($page < 1) {
            // выходим — пользователь зашел по неверной ссылке
            $this->_abort('Incorrect page specified');
        }

        // составляем запрос получения данных
        $query = $this->_getQuery($page);

        // вот они, данные
        $set = $this->_getRecordSet($query);

        $total = $set->total();

        // если список непустой, но номер указанная страницы — больше, чем страниц всего
        if ($total > 0 && $this->_per_page > 0 && $page > ceil($total / $this->_per_page)) {

            // выходим - пользователь зашел по неверной ссылке
            $this->_abort('Incorrect page specified');
        }

        // если список получился пустым, а вывод пустого списка недопустим
        if (0 == count($entries = $set->entries()) && !$this->_allow_empty) {

            // выходим, дальше делать нечего
            $this->_abort('No entries found');
        }

        Application::getResponse()->assign(array(
            'entries' => $entries,
            'total' => $set->total(),
            'page' => $page,
            'perpage' => $this->_per_page,
        ));
    }

    /**
     * Формирует запрос выборки записей страницы. Запрос должен содержать
     * инструкцию SQL_CALC_FOUND_ROWS для списков с постраничным разбиением.
     *
     * @return  string
     */
    abstract protected function _getQueryBody();

    /**
     * Оборачивает записи и их общее количество в объект.
     *
     * @param   array   $entries
     * @param   integer $total
     * @return  Kernel_RecordSet
     */
    protected function _wrap($entries, $total) {
        return new Kernel_RecordSet($entries, $total);
    }

    /**
     * Возвращает порядковый номер страницы, которую нужно показать.
     *
     * @return  integer
     */
    protected function _getPage() {
        $request = Application::getRequest();
        return is_null($id = $request->get('id'))
            ? (int)$request->get('page', 1)
            : ($this->_per_page > 0
                ? $this->_getRecordPage($id)
                : 1);
    }

    /**
     * Возвращает SQL-условие фильтрации записей.
     *
     */
    protected function _getFilter() {
        $params = array();
        foreach ($this->filter as $param => $value) {
            $params[isset($this->_filter_map[$param])
                ? $this->_filter_map[$param] : $param] = $value;
        }
        return database::condition(array_filter($params));
    }

    /**
     * Формирует запрос выборки записей страницы указанной страницы.
     *
     * @param   integer $page
     * @return  string
     */
    private function _getQuery($page) {
        $query = $this->_getQueryBody();
        if ($this->_per_page > 0) {
            if (!$this->_isPaginated($query)) {
                trigger_error('"SQL_CALC_FOUND_ROWS" option has to be included into query', E_USER_ERROR);
            }
            $query .= '
                   LIMIT
                         ' . (($page - 1) * $this->_per_page) . ', ' . $this->_per_page;
        }
        return $query;
    }

    /**
     * Возвращает набор данных, полученных по указанному запросу.
     *
     * @param   string  $query
     * @return  Kernel_RecordSet
     */
    private function _getRecordSet($query) {

        // оборачиваем результат
        return $this->_wrap(

            // получаем массив данных из запроса
            database::fetchAll($query),

            $this->_isPaginated($query)

                // получаем общее количество записей без учета LIMIT
                ? database::fetchOne('SELECT FOUND_ROWS()') : null);
    }

    /**
     * Определяет, поддерживает ли конструкция запроса постраничное разбиение.
     *
     * @param   string  $query
     * @return  boolean
     */
    private function _isPaginated($query) {
        return false !== stripos($query, 'SQL_CALC_FOUND_ROWS');
    }

    /**
     * Возвращает порядковый номер страницы, на которой находится запись
     * с указанным идентификатором.
     *
     * @param   integer $id
     * @return  integer
     */
    private function _getRecordPage($id) {
        $table = 'tmp_list';

        database::query('CREATE TEMPORARY TABLE `' . $table . '` (
            `position` int unsigned NOT NULL auto_increment,
            `id` int unsigned,
            PRIMARY KEY (`position`),
            UNIQUE KEY(`id`)
        )');

        database::query('INSERT INTO `' . $table . '` (`id`)
            SELECT
                   `id`
              FROM
                    (' . str_replace('SQL_CALC_FOUND_ROWS', '', $this->_getQueryBody()) . ') `dummy`');

        $query = 'SELECT
                         `position`
                    FROM
                         `' . $table . '`
                   WHERE
                         `id` = ' . (int)$id;

        $position = database::fetchOne($query);

        database::query('DROP TABLE `' . $table . '`');

        return !is_null($position)
            ? ceil($position / $this->_per_page) : null;
    }
}
